﻿using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Runtime.CompilerServices;
namespace SharpObservation
{
    /// <summary>
    /// Provides helper methods for efficiently disposing of objects
    /// </summary>
    /// <remarks>
    /// Typically, you will simply derive from DisposableObject, DisposableObservableObject or DisposableEventsObject.
    /// 
    /// To leverage this utility directly in your own class:
    /// <code>
    /// public class MyClass : IDisposable      // implement disposable
    /// {
    /// 
    ///     public void Dispose()
    ///     {
    ///        DisposeHelper.DisposeOf(this, true);     // Call DisposeOf in the dispose implementation.
    ///     }
    ///     
    ///     ~MyClass()                                              // OPTIONALLY implement a finalizer.  
    ///     {                                                       // Finalizers incur a penalty with the garbage collector, and should 
    ///         DisposeHelper.DisposeOf(this, false);               // only be implemented if your class references unmanaged resources.    
    ///     }                                                       // The helper will call GC>SurpressFinalize if a finalizer exists .
    /// 
    ///     protected virtual void Dispose(bool disposing)          // OPTIONALLY implement a Dispose virtual method.   The helper will call it if it exists.
    ///     {
    ///     }
    /// 
    ///     private bool isDisposed;                                // OPTIONAL.    If exists, the helper will update the value to true once disposed.
    ///     public bool IsDisposed { get { return isDisposed; } }
    /// 
    ///     private Action throwIfDisposed =  () => { };            // OPTIONAL.    If exists, the helper will change the delegate to throw an ObjectDisposedException
    ///     protected Action ThrowIfDisposed { get { return throwIfDisposed; } }
    /// </code>
    /// </remarks> 
    public class DisposeHelper
    {
        /// <summary>
        /// Disposes of an object
        /// </summary>
        /// <param name="disposable">The object to dispose of</param>
        /// <param name="disposing">true if called from Dispose, false if called from finalizer</param>
        /// <remarks>
        /// Disposes of an object by performing the following steps:
        /// 
        /// 1) If disposable is null, it exits
        /// 2) If disposable has an isDisposed field, and the field is true, it exists.
        ///    Otherwise the field is assigned "true".
        /// 3) If the victim has a throwIfDisposed delegate field, it is assigned DisposedExceptionThrower.
        /// 4) If Disposing and a finalizer has been defined, GC.SurpressFinalizer is called.
        /// 4) If Disposing, all reference type fields are inspected:
        ///         IDisposable instances are disposed.
        ///         Arrays are inspected for disposable instanced, and then elements are nulled.
        /// 5) Regardless of Disposing, all reference type fields are set to null.
        /// </remarks>
        public static void DisposeOf(IDisposable disposable, bool disposing)
        {
            DisposerMethod doDispose = null;
            var type = disposable.GetType();

            if (!Disposers.TryGetValue(type, out doDispose))
                Disposers.Add(type, doDispose = CreateDisposer(type));

            doDispose(disposable, disposing);
        }

        private delegate void DisposerMethod(object victim, bool disposing);

        private static IDictionary<Type, DisposerMethod> Disposers = new Dictionary<Type, DisposerMethod> ();

        /// <summary>
        /// Helper Delegate for throwing exceptions after an object has been disposed. Assigned to throwIfDisposed fields in disposed instances.
        /// </summary>
        private static Action DisposedExceptionThrower = () => { throw new ObjectDisposedException("Object"); };


        /// <summary>
        /// Helper Method. Disposes of an array if potentially disposable references.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="array"></param>
        private static void DisposeArray<T>(T[] array) where T : class, IDisposable
        {
            if (array == null)
                return;

            for (int i = 0; i < array.Length; i++)
            {
                if (array[i] != null)
                    array[i].Dispose();

                array[i] = null;
            }
        }


        private static IEnumerable<FieldInfo> GetFields(Type type)
        {
            for(var t = type; t != typeof(object); t = t.BaseType)
                foreach(var f in t.GetFields(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic))
                    yield return f;
        }

        private static DisposerMethod CreateDisposer(Type type)
        {
            var allfields = GetFields(type).ToArray();
            var isDisposedField = allfields.FirstOrDefault(f => f.FieldType == typeof(bool) && f.Name.ToLowerInvariant().EndsWith("isdisposed"));
            var throwIfDisposedField = allfields.FirstOrDefault(f => f.FieldType.IsAssignableFrom(typeof(Action)) && f.Name.ToLowerInvariant().EndsWith("throwifdisposed"));
            var finalizerMethod = type.GetMethod("Finalize", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
            var disposeMethod = type.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
                .FirstOrDefault(y => y.Name == "Dispose"
                                  && y.IsVirtual
                                  && y.GetParameters().Length == 1
                                  && y.GetParameters()[0].ParameterType == typeof(bool));

            var m = new DynamicMethod("Dispose" + type.Name,
                typeof(void),
                new Type[] { typeof(object), typeof(bool) },
                typeof(DisposeHelper).Module,
                true);

            var il = m.GetILGenerator();
            var thisDisposable = il.DeclareLocal(typeof(IDisposable));
            var thisRef = il.DeclareLocal(type);
            var thisIndex = il.DeclareLocal(typeof(int));
            var thisLength = il.DeclareLocal(typeof(int));
            var exitLabel = il.DefineLabel();

            // 1.Check if the reference is of the correct type.If not, throw an exception.
            // if(thisRef == null) return;
            il.Emit(OpCodes.Ldarg_0); 
            il.Emit(OpCodes.Castclass, type);
            il.Emit(OpCodes.Stloc, thisRef);
            //il.Emit(OpCodes.Brfalse, exitLabel);

            // 2. Check if type implements IsDisposed field. If so, check for true - if true, exit,  else set IsDisposed = true
            if (isDisposedField != null)
            {
                // if(thisRef.IsDisposed) goto exitLabel;
                il.Emit(OpCodes.Ldloc, thisRef);
                il.Emit(OpCodes.Ldfld, isDisposedField);
                il.Emit(OpCodes.Brtrue, exitLabel);

                // thisRef.IsDisposed = true;
                il.Emit(OpCodes.Ldloc, thisRef);
                il.Emit(OpCodes.Ldc_I4_1);
                il.Emit(OpCodes.Stfld, isDisposedField);
            }

            // 3. Check if type implements ThrowIfDisposed. If so, change if to point to new delegate.
            if (throwIfDisposedField != null)
            {
                // thisRef.ThrowIfDisposed = DisposedExceptionThrower;
                il.Emit(OpCodes.Ldloc, thisRef);
                var fld = typeof(DisposeHelper).GetField(nameof(DisposedExceptionThrower), BindingFlags.Static | BindingFlags.NonPublic);
                il.Emit(OpCodes.Ldsfld, fld);
                il.Emit(OpCodes.Stfld, throwIfDisposedField);
            }

            // 4. Check if object implements virtual Dispose(bool). If so, invoke the method.
            if (disposeMethod != null)
            {
                il.Emit(OpCodes.Ldloc, thisRef);
                il.Emit(OpCodes.Ldarg_1);
                il.Emit(OpCodes.Callvirt, disposeMethod);
            }
            
            // 5. If a finalizer has been defined on the object and Disposing = true, surpress finalization
            if (finalizerMethod != null)
            {
                // if(!disposing) GC.SuppressFinalize(thisRef);
                var notDisposing = il.DefineLabel();
                il.Emit(OpCodes.Ldarg_1);
                il.Emit(OpCodes.Brfalse_S, notDisposing);
                il.Emit(OpCodes.Ldloc, thisRef);
                il.Emit(OpCodes.Call, typeof(GC).GetMethod("SuppressFinalize"));
                il.MarkLabel(notDisposing);
            }

            // 6. Traverse all reference fields. IF referencing a disposable object, dispose of it. Else set to null.
            foreach (var field in allfields)
            {
                var fieldType = field.FieldType;
                if (!typeof(IDisposable).IsAssignableFrom(fieldType) && !fieldType.IsArray)
                    continue;

                var nextField = il.DefineLabel();
                var nullifyField = il.DefineLabel();

                // if(thisRef.field != null)
                il.Emit(OpCodes.Ldloc, thisRef);
                il.Emit(OpCodes.Ldfld, field);
                il.Emit(OpCodes.Brfalse_S, nextField);

                //   if(Disposing) {
                il.Emit(OpCodes.Ldarg_1);
                il.Emit(OpCodes.Brfalse_S, nullifyField);

                if (fieldType.IsArray)
                {
                    var elementType = fieldType.GetElementType();
                    if (typeof(IDisposable).IsAssignableFrom(elementType))
                    {
                        var method = typeof(DisposeHelper)
                                    .GetMethod(nameof(DisposeArray), BindingFlags.Static | BindingFlags.NonPublic)
                                    .MakeGenericMethod(new[] { elementType });
                        il.Emit(OpCodes.Ldloc, thisRef);
                        il.Emit(OpCodes.Ldfld, field);
                        il.Emit(OpCodes.Call, method);
                    }
                }
                else if (typeof(IDisposable).IsAssignableFrom(fieldType))
                {
                    // var thisDisposable = thisRef.Field as IDisposable;
                    // if(thisDisposable != null) disposable.Dispose();
                    il.Emit(OpCodes.Ldloc, thisRef);
                    il.Emit(OpCodes.Ldfld, field);
                    il.Emit(OpCodes.Isinst, typeof(IDisposable));
                    il.Emit(OpCodes.Stloc, thisDisposable);
                    il.Emit(OpCodes.Ldloc, thisDisposable);
                    il.Emit(OpCodes.Brfalse_S, nullifyField);
                    il.Emit(OpCodes.Ldloc, thisDisposable);
                    il.Emit(OpCodes.Callvirt, typeof(IDisposable).GetMethod("Dispose"));
                }
                // thisRef.Field = null;
                il.MarkLabel(nullifyField);
                il.Emit(OpCodes.Ldloc, thisRef);
                il.Emit(OpCodes.Ldnull);
                il.Emit(OpCodes.Stfld, field);
                il.MarkLabel(nextField);
            }

            il.MarkLabel(exitLabel);
            il.Emit(OpCodes.Ret);

            return (DisposerMethod)m.CreateDelegate(typeof(DisposerMethod));
        }
    }
}
